using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;

using ExchangeHog.Pop3Client;
using ExchangeHog.Pop3Client.Responses;

namespace ExchangeHog.Pop3Client.Commands
{
	internal abstract class Pop3Command<T> : IDisposable where T : Pop3Response
	{
		public event Action<string> Trace;
		protected void OnTrace(string message) { if (Trace != null) Trace(message); }

		private const int BUFFER_SIZE = 1024;
		private const string POP3_MULTILINEMESSAGETERMINATOR = "\r\n.\r\n";
		private const string POP3_MESSAGETERMINATOR = ".";

		private ManualResetEvent mreResponseIsReady;

		private byte[] buffer;
		private MemoryStream responseContents;

		public Pop3State ValidExecuteState { get; private set; }
		public Stream NetworkStream { get; set; }
		protected bool IsMultiline { get; set; }

		public Pop3Command(Stream stream, bool isMultiline, Pop3State validExecuteState)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");

			mreResponseIsReady = new ManualResetEvent(false);
			buffer = new byte[BUFFER_SIZE];
			responseContents = new MemoryStream();
			NetworkStream = stream;
			IsMultiline = isMultiline;
			ValidExecuteState = validExecuteState;
		}

		protected abstract byte[] CreateRequestMessage();
		private void Send(byte[] message)
		{
			try
			{
				NetworkStream.Write(message, 0, message.Length);
			}
			catch (SocketException e)
			{
				throw new Pop3Exception("Unable to send the request message: " + Encoding.ASCII.GetString(message), e);
			}
		}

		internal virtual T Execute(Pop3State currentState)
		{
			EnsurePop3State(currentState);

			byte[] message = CreateRequestMessage();

			if (message != null)
				Send(message);

			T response = CreateResponse(GetResponse());
			if (response != null)
				OnTrace(response.HostMessage);

			return response;
		}

		protected void EnsurePop3State(Pop3State currentState)
		{
			if (!((currentState & ValidExecuteState) == currentState))
				throw new Pop3Exception(
					string.Format("This command is being executed in an invalid execution state.  Current:{0}, Valid:{1}",
					currentState, ValidExecuteState));
		}

		protected virtual T CreateResponse(byte[] buffer)
		{
			return Pop3Response.CreateResponse(buffer) as T;
		}

		private byte[] GetResponse()
		{
			try
			{
				Receive(
					IsMultiline ?
						new AsyncCallback(GetMultiLineResponseCallback) :
						new AsyncCallback(GetSingleLineResponseCallback)
				);

				mreResponseIsReady.WaitOne();

				return responseContents.ToArray();
			}
			catch (SocketException e)
			{
				throw new Pop3Exception("Unable to get response.", e);
			}
		}

		private IAsyncResult Receive(AsyncCallback callback)
		{
			return NetworkStream.BeginRead(buffer, 0, buffer.Length, callback, null);
		}

		private string WriteReceivedBytesToBuffer(int bytesReceived)
		{
			responseContents.Write(buffer, 0, bytesReceived);
			byte[] contents = responseContents.ToArray();
			return Encoding.ASCII.GetString(contents, (contents.Length > 5 ? contents.Length - 5 : 0), 5);
		}

		private void GetSingleLineResponseCallback(IAsyncResult ar)
		{
			int bytesReceived = NetworkStream.EndRead(ar);
			string message = WriteReceivedBytesToBuffer(bytesReceived);

			if (message.EndsWith(Pop3Commands.Crlf))
				mreResponseIsReady.Set();
			else
				Receive(new AsyncCallback(GetSingleLineResponseCallback));
		}

		private void GetMultiLineResponseCallback(IAsyncResult ar)
		{
			int bytesReceived = NetworkStream.EndRead(ar);
			string message = WriteReceivedBytesToBuffer(bytesReceived);
			//
			//	if the POP3 server times out we'll get an error message, then we'll get a following callback w/ 0 bytes.
			//
			if (message.EndsWith(POP3_MULTILINEMESSAGETERMINATOR) || bytesReceived == 0)
				mreResponseIsReady.Set();
			else
				Receive(new AsyncCallback(GetMultiLineResponseCallback));
		}

		protected byte[] GetRequestMessage(params string[] args)
		{
			string message = string.Join(string.Empty, args);
			OnTrace(message);
			return Encoding.ASCII.GetBytes(message);
		}

		protected MemoryStream StripPop3HostMessage(byte[] bytes, string header)
		{
			int position = header.Length + 2;
			return new MemoryStream(bytes, position, bytes.Length - position);
		}

		protected string[] GetResponseLines(MemoryStream stream)
		{
			List<string> lines = new List<string>();
			using (StreamReader reader = new StreamReader(stream))
			{
				try
				{
					do
					{
						string line = reader.ReadLine();

						//
						//	pop3 protocol states if a line starts w/ a '.' that line will be byte stuffed w/ a '.'
						//	if it is byte stuffed the remove the byte, otherwise we have reached the end of the message.
						//
						if (line.StartsWith(POP3_MESSAGETERMINATOR))
						{
							if (line.CompareTo(POP3_MESSAGETERMINATOR) == 0)
								break;

							line = line.Substring(1);
						}
						lines.Add(line);
					} while (true);
				}
				catch (IOException e)
				{
					throw new Pop3Exception("Unable to get response lines.", e);
				}

				return lines.ToArray();
			}
		}

		public void Dispose()
		{
			if (responseContents != null)
				responseContents.Dispose();
		}
	}
}